Skip to content

pipewire: context hardening — combines #226 + #227, reconciled#229

Merged
jcelerier merged 2 commits into
masterfrom
fix/pipewire-context-hardening
Jul 13, 2026
Merged

pipewire: context hardening — combines #226 + #227, reconciled#229
jcelerier merged 2 commits into
masterfrom
fix/pipewire-context-hardening

Conversation

@jcelerier

@jcelerier jcelerier commented Jul 10, 2026

Copy link
Copy Markdown
Member

Combines #226 (@ogauthiersat) and #227 into one coherent series, with the two invoke_sync fixes reconciled — merging both PRs as-is would auto-merge cleanly and deadlock (each adds its own lock around the blocking invoke; the recursive mutex would be held twice, and the loop-control hooks release exactly one level, so the worker could never dispatch the invoke).

Contents:

Validation: ossia score's PipewireRoundtrip harness against a live PipeWire 1.3.0 daemon — 28-cell format×transport matrix (SHM + DMA-BUF, OpenGL + Vulkan, 12 pixel formats incl. padded strides): 24 PASS + 4 PASS(honest-fallback), zero deadlocks/asserts across repeated stream create/destroy cycles. #226's regression examples compile and are included unchanged.

Supersedes #227 (I'll close it); #226 can be closed in favor of this or merged first — this branch contains it verbatim either way.

🤖 Generated with Claude Code

https://claude.ai/code/session_014rZgzE8JjWvHDtaVUhxpLE


Update (91ecbd5): invoke_sync is now rewritten to a non-blocking pw_loop_invoke + private mutex/CV with a sync_deadline bound, instead of a lock-held blocking invoke. Rationale: the blocking-invoke lock contract flip-flopped across PipeWire releases (hooks removed/reverted three times upstream; 1.3.0–1.3.80 and 1.5.0–1.5.80 dev clients deadlock with a held lock, all 1.2/1.4 releases require it, 1.5.81+ accepts either) — the non-blocking pattern is contract-stable on every series. Details and the per-series matrix in the comments below. Validated with score's 28-cell round-trip harness and the three regression examples from #226.

@jcelerier

Copy link
Copy Markdown
Member Author

Follow-up commit 91ecbd5: invoke_sync rewritten to be correct on every PipeWire series, addressing the 1.4-era concerns.

Version archaeology (verified per tag in the pipewire sources, spa/plugins/support/loop.c blocking-wait section): upstream removed and re-reverted the loop-control-hook calls around the blocking-invoke wait three times, so the lock contract of pw_loop_invoke(block=true) from a foreign thread depends on which client library you run against:

PipeWire client blocking invoke: hooks fired correct caller pattern
1.2.0–1.2.8, 1.3.81–1.3.83, 1.4.0–1.4.11 (all releases) yes lock held exactly once
1.3.0–1.3.80, 1.5.0–1.5.80 (dev series, shipped by some distros) no lock must NOT be held → deadlocks otherwise
1.5.81+, 1.6.0–1.6.8 no (self-managing wait) either

No blocking invoke is correct against all three states — so the new invoke_sync doesn't use one: it queues a non-blocking invoke (fires no hooks, needs no lock, identical on every series) and waits on a private mutex/CV that the trampoline notifies, bounded by sync_deadline so teardown races return instead of hanging. An abandoned handshake under the payload mutex guarantees by-reference captures are either consumed before a timeout is observed or never touched, and the shared_ptr-owned payload prevents use-after-free on either side.

Validation on this branch: ossia score's 28-cell PipeWire round-trip matrix (24 PASS + 4 honest-fallback, zero recurse asserts) plus the three watchdog-armed regression examples from #226pipewire_context_sync (320k synchronize() round-trips), pipewire_context_reconnect (8 generations), pipewire_context_subscriptions (incl. on-loop-thread synchronize). Note for packagers: on 1.3.0–1.3.80 / 1.5.0–1.5.80 dev clients, PipeWire's own pw_stream internals are broken regardless of what client code does (that is why upstream reverted twice) — this change just ensures libremidi adds no hazard of its own there.

@jcelerier

jcelerier commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@pri-yan-shu do you think you could test this last pipewire PR ? As it involves the pipewire work which had deadlock / crash issues depending on the linux distro and pipewire version

@jcelerier jcelerier force-pushed the fix/pipewire-context-hardening branch from 91ecbd5 to 86d7e1d Compare July 11, 2026 21:53
@jcelerier

jcelerier commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@ogauthiersat I refactored and moved the PW tests you added with the other tests, if you can also test again :)

@jcelerier jcelerier force-pushed the fix/pipewire-context-hardening branch 2 times, most recently from 70682d0 to ae628c7 Compare July 13, 2026 02:39
@pri-yan-shu

Copy link
Copy Markdown

I'm on pipewire 1.6.7, all tests pass. Anything else I can test?

@jcelerier

Copy link
Copy Markdown
Member Author

thanks @pri-yan-shu !

jcelerier and others added 2 commits July 13, 2026 12:43
Fix thread-loop lock and iteration bugs in the shared PipeWire context,
surfaced by concurrent audio + MIDI use and by video round-trip testing:

- synchronize(): a foreign-thread sync no longer iterates the pw_loop
  itself (two threads driving one loop corrupt its per-source dispatch
  state and crash in the core-socket callback) nor holds pipewire's
  recursive lock across the wait. It issues pw_core_sync under a brief
  lock and waits on a private condition variable that the core done/error
  callbacks notify.

- reconnect(): runs on the calling thread instead of via invoke_sync on
  the worker. tear_down() destroys the very loop the worker runs, so
  reconnecting from it self-destructs the loop and hangs.

- invoke_sync(): queues a non-blocking pw_loop_invoke and waits on its own
  condition variable, bounded by sync_deadline. The blocking-invoke lock
  contract is unstable across PipeWire versions; the non-blocking path is
  correct on every release.

- format negotiation advertises buffer size/stride as ranges so producers
  with padded rows (GPU-allocated strides) can link.

Co-Authored-By: Olivier Gauthier <ogauthier@sat.qc.ca>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three standalone programs under tests/integration/ exercising the
thread-loop context fixes: sync (create/synchronize/destroy churn and a
sustained synchronize loop), reconnect (repeated reconnect from the
calling thread), and subscriptions (subscribe/unsubscribe churn, filter
teardown with live subscriptions, and synchronize from a loop-thread
callback).

Each skips with exit 0 when no PipeWire daemon is reachable and arms a
watchdog so a lock-corruption regression fails loudly instead of hanging.

Co-Authored-By: Olivier Gauthier <ogauthier@sat.qc.ca>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jcelerier jcelerier force-pushed the fix/pipewire-context-hardening branch from ae628c7 to 75a3d27 Compare July 13, 2026 16:43
@ogauthiersat

Copy link
Copy Markdown
Contributor

@jcelerier All tests passes on trixie with pipewire 1.4.2. I also test with score, it resolves the crash I had in pipewire client at process exit.

I could successfully stream a midi file from Ardour. I tested restarting audio engine while having a midi device connected and it appears to behave correctly.

@jcelerier

Copy link
Copy Markdown
Member Author

\o/ thanks all !

@jcelerier jcelerier merged commit 67e8ccd into master Jul 13, 2026
90 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants